﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BigData.DataContracts.Objects;
using BigDataFramework.Repositories.Interfaces;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using System.Linq.Expressions;
using MongoDB.Driver.Builders;
using BigData.DataContracts.Objects.BigData.BaseTypes;
using BigDataFramework.Library.Helpers;
using BigData.DataContracts.Objects.BigData;

namespace BigDataFramework.Repositories
{
    public class MongoDBRepository : IRepository
    {
        #region Private Members
        /// <summary>
        /// The server from which the MongoDBRepository is to pull all of it's data
        /// </summary>
        private MongoServer _Server { get; set; }
        /// <summary>
        /// The database from which the MongoDBRepository is to pull all of it's data
        /// </summary>
        private MongoDatabase _Database { get; set; }
        /// <summary>
        /// A lazy loaded BigDataObject to MongoCollection mapper
        /// Key: The Type.FullName of object
        /// Value: The collection which the Key is associated with
        /// Note:
        /// 1) This can be populated when initializing the MongoDBRepository in order for the user to map objects
        /// 2) Otherwise, this is lazy loaded, which allows the user to instantiate this class multiple times without as much fear of memory overhead issues
        /// </summary>
        public Dictionary<string, MongoCollection> _MongoCollectionMapper { get; set; }
        #endregion Private Members
        #region Private Methods
        /// <summary>
        /// Returns the mongoDB collection which corresponds to the passed in Type
        /// If one does not exists, this will attempt to create one
        /// </summary>
        /// <param name="T">The type of object to get the corresponding collection of</param>
        /// <returns>The mongo collection which correspond to the the passed in object</returns>
        private MongoCollection GetMongoCollection(Type T)
        {
            string collectionName;
            string typeName = T.FullName;

            // If the collection already exists, return it
            if (this._MongoCollectionMapper.ContainsKey(typeName))
                return this._MongoCollectionMapper[typeName];

            collectionName = MongoDBRepositoryHelper.ConvertTypeToCollectionName(T);

            // Retrieve/create the collection
            MongoCollection collection = this._Database.GetCollection(collectionName);

            // Make sure the returned collection is valid
            if (collection == null)
                throw new Exception(string.Format("Unable to determine the MongoDB collection corresponding to type: {0}", collectionName));

            // Add the collection to the registered collections object
            this._MongoCollectionMapper.Add(typeName, collection);

            // Return the new collection
            return collection;
        }
        #endregion Private Methods

        public MongoDBRepository()
            : this(
                    connectionString: MongoDBRepositoryHelper.CONNECTIONSTRING,
                    databaseName: MongoDBRepositoryHelper.DATABASENAME,
                    objectTypeToCollectionMapper: MongoDBRepositoryHelper.RegisteredCollections
                ) { }

        public MongoDBRepository(string connectionString, string databaseName, Dictionary<string, string> objectTypeToCollectionMapper)
        {
            this._Server    = MongoServer.Create(connectionString);
            this._Database  = this._Server.GetDatabase(databaseName);

            this._MongoCollectionMapper = new Dictionary<string, MongoCollection>();

            foreach (string typeName in objectTypeToCollectionMapper.Keys)
                this._MongoCollectionMapper.Add(typeName, this._Database.GetCollection(objectTypeToCollectionMapper[typeName]));
        }

        #region Public Methods
        #region Create
        /// <summary>
        /// Creates an object of type t in it's corresponding collection in the database. If the corresponding collection does not exist then mongo will attempt to create it
        /// </summary>
        /// <typeparam name="T">the type of object to insert into the database</typeparam>
        /// <param name="obj">the object to insert into the database</param>
        public void Create<T>(T obj)
        {
            this.GetMongoCollection(typeof(T)).Insert<T>(obj);
        }

        /// <summary>
        /// Creates a list of objects of type t in their corresponding collection in the database. If the corresponding collection does not exist then mongo will attempt to create it
        /// </summary>
        /// <typeparam name="T">The type of object to insert into the database</typeparam>
        /// <param name="objs">A list of objects to insert into the database</param>
        public void Create<T>(List<T> objs)
        {
            this.GetMongoCollection(typeof(T)).InsertBatch(objs);
        } 
        #endregion Create
        #region Read
        /// <summary>
        /// Returns a queryable which contains the objects in the database filtered by the passed in predicate object
        /// </summary>
        /// <typeparam name="T">The type of object to read from the database</typeparam>
        /// <param name="predicate">An expression to filter the results of the mongo collection</param>
        /// <returns>A queryable which contains the objects in the database filtered by the passed in predicate object</returns>
        public IQueryable<T> Read<T>(Expression<Func<T, bool>> predicate = null)
        {
            if (predicate == null)
                return this.GetMongoCollection(typeof(T)).FindAllAs<T>().AsQueryable();

            // I've read that the LINQ query is executed on the server, therefore this should be fine
            return this.GetMongoCollection(typeof(T)).AsQueryable<T>().Where(predicate);
        }

        public T ReadOne<T>(Expression<Func<T, bool>> predicate)
        {
            List<T> output = this.Read<T>(predicate).Take(2).ToList();

            // TODO: Make sure this returns a valid error
            if (output.Count < 1)
                throw new Exception(string.Format("Unable to return a valid object of type {0} for predicate {1}", typeof(T), predicate.Body.ToString()));

            if (output.Count > 1)
                throw new Exception(string.Format("Unable to return distinct valid object of type {0} for predicate {1}", typeof(T), predicate.Body.ToString()));

            return output.Single();
        }

        public T ReadOne<T>(string id)
            where T : BigDataDBObjectBase
        {
            ObjectId oid = MongoDBRepositoryHelper.ConvertStringToObjectId(id);

            return this.ReadOne<T>(oid);
        }

        public T ReadOne<T>(ObjectId oid)
            where T : BigDataDBObjectBase
        {
            return this.ReadOne<T>(t => t._id == oid.ToString());
        }
        #endregion Read

        /// <summary>
        /// Updates an object specified by its id in the collection specified by T
        /// </summary>
        /// <typeparam name="T">The type of object to update</typeparam>
        /// <param name="id">The id of the object to update</param>
        /// <param name="obj"></param>
        public void Update<T>(T obj)
            where T : BigDataDBObjectBase
        {
            this.GetMongoCollection(typeof(T)).Save(typeof(T), obj);
        }

        /// <summary>
        /// Deletes an object specified by id in the collection specified by T
        /// </summary>
        /// <typeparam name="T">The type of object to delete</typeparam>
        /// <param name="id">The id of the object to delete</param>
        public void Delete<T>(T obj)
             where T : BigDataDBObjectBase
        {
            var query = Query.EQ("_id", ObjectId.Parse(obj._id));

            this.GetMongoCollection(typeof(T)).Remove(query, RemoveFlags.Single);
        }
    }
        #endregion Public Methods
}
